{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Modeling and Simulation in Python\n",
    "\n",
    "Chapter 25\n",
    "\n",
    "Copyright 2017 Allen Downey\n",
    "\n",
    "License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Configure Jupyter so figures appear in the notebook\n",
    "%matplotlib inline\n",
    "\n",
    "# Configure Jupyter to display the assigned value after an assignment\n",
    "%config InteractiveShell.ast_node_interactivity='last_expr_or_assign'\n",
    "\n",
    "# import functions from the modsim.py module\n",
    "from modsim import *"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Teapots and Turntables"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tables in Chinese restaurants often have a rotating tray or turntable that makes it easy for customers to share dishes.  These turntables are supported by low-friction bearings that allow them to turn easily and glide.  However, they can be heavy, especially when they are loaded with food, so they have a high moment of inertia.\n",
    "\n",
    "Suppose I am sitting at a table with a pot of tea on the turntable directly in front of me, and the person sitting directly opposite asks me to pass the tea.  I push on the edge of the turntable with 1 Newton of force until it has turned 0.5 radians, then let go.  The turntable glides until it comes to a stop 1.5 radians from the starting position.  How much force should I apply for a second push so the teapot glides to a stop directly opposite me?\n",
    "\n",
    "The following figure shows the scenario, where `F` is the force I apply to the turntable at the perimeter, perpendicular to the moment arm, `r`, and `tau` is the resulting torque.  The blue circle near the bottom is the teapot.\n",
    "\n",
    "![](diagrams/teapot.png)\n",
    "\n",
    "We'll answer this question in these steps:\n",
    "\n",
    "1.  We'll use the results from the first push to estimate the coefficient of friction for the turntable.\n",
    "\n",
    "2.  We'll use that coefficient of friction to estimate the force needed to rotate the turntable through the remaining angle.\n",
    "\n",
    "Our simulation will use the following parameters:\n",
    "\n",
    "1.  The radius of the turntable is 0.5 meters, and its weight is 7 kg.\n",
    "\n",
    "2.  The teapot weights 0.3 kg, and it sits 0.4 meters from the center of the turntable.\n",
    "\n",
    "As usual, I'll get units from Pint."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "radian = UNITS.radian\n",
    "m = UNITS.meter\n",
    "s = UNITS.second\n",
    "kg = UNITS.kilogram\n",
    "N = UNITS.newton"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And store the parameters in a `Params` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = Params(radius_disk=0.5*m,\n",
    "                mass_disk=7*kg,\n",
    "                radius_pot=0.4*m,\n",
    "                mass_pot=0.3*kg,\n",
    "                force=1*N,\n",
    "                torque_friction=0.2*N*m,\n",
    "                theta_end=0.5*radian,\n",
    "                t_end=20*s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`make_system` creates the initial state, `init`, and computes the total moment of inertia for the turntable and the teapot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_system(params):\n",
    "    \"\"\"Make a system object.\n",
    "    \n",
    "    params: Params object\n",
    "    \n",
    "    returns: System object\n",
    "    \"\"\"\n",
    "    mass_disk, mass_pot = params.mass_disk, params.mass_pot\n",
    "    radius_disk, radius_pot = params.radius_disk, params.radius_pot\n",
    "    \n",
    "    init = State(theta=0*radian, omega=0*radian/s)\n",
    "    \n",
    "    I_disk = mass_disk * radius_disk**2 / 2\n",
    "    I_pot = mass_pot * radius_pot**2\n",
    "    \n",
    "    return System(params, init=init, I=I_disk+I_pot)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's the `System` object we'll use for the first phase of the simulation, while I am pushing the turntable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "system1 = make_system(params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Simulation\n",
    "\n",
    "When I stop pushing on the turntable, the angular acceleration changes abruptly.  We could implement the slope function with an `if` statement that checks the value of `theta` and sets `force` accordingly.  And for a coarse model like this one, that might be fine.  But we will get more accurate results if we simulate the system in two phases:\n",
    "\n",
    "1.  During the first phase, force is constant, and we run until `theta` is 0.5 radians.\n",
    "\n",
    "2.  During the second phase, force is 0, and we run until `omega` is 0.\n",
    "\n",
    "Then we can combine the results of the two phases into a single `TimeFrame`.\n",
    "\n",
    "Here's the slope function we'll use:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def slope_func(state, t, system):\n",
    "    \"\"\"Computes the derivatives of the state variables.\n",
    "    \n",
    "    state: State object\n",
    "    t: time\n",
    "    system: System object \n",
    "    \n",
    "    returns: sequence of derivatives\n",
    "    \"\"\"\n",
    "    theta, omega = state\n",
    "    radius_disk, force = system.radius_disk, system.force\n",
    "    torque_friction, I = system.torque_friction, system.I\n",
    "    \n",
    "    torque = radius_disk * force - torque_friction\n",
    "    alpha = torque / I\n",
    "    \n",
    "    return omega, alpha "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As always, we'll test the slope function before running the simulation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "slope_func(system1.init, 0, system1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's an event function that stops the simulation when `theta` reaches `theta_end`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def event_func1(state, t, system):\n",
    "    \"\"\"Stops when theta reaches theta_end.\n",
    "    \n",
    "    state: State object\n",
    "    t: time\n",
    "    system: System object \n",
    "    \n",
    "    returns: difference from target\n",
    "    \"\"\"\n",
    "    theta, omega = state\n",
    "    return theta - system.theta_end "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "event_func1(system1.init, 0, system1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can run the first phase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "results1, details1 = run_ode_solver(system1, slope_func, events=event_func1)\n",
    "details1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And look at the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "results1.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Phase 2\n",
    "\n",
    "Before we run the second phase, we have to extract the final time and state of the first phase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "t_0 = results1.last_label() * s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And make an initial `State` object for Phase 2."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "init2 = results1.last_row()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And a new `System` object with zero force."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "system2 = System(system1, t_0=t_0, init=init2, force=0*N)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's an event function that stops when angular velocity is 0."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "def event_func2(state, t, system):\n",
    "    \"\"\"Stops when omega is 0.\n",
    "    \n",
    "    state: State object\n",
    "    t: time\n",
    "    system: System object \n",
    "    \n",
    "    returns: omega\n",
    "    \"\"\"\n",
    "    theta, omega = state\n",
    "    return omega"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "event_func2(system2.init, 0, system2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can run the second phase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "slope_func(system2.init, system2.t_0, system2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "results2, details2 = run_ode_solver(system2, slope_func, events=event_func2)\n",
    "details2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And check the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "results2.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pandas provides `combine_first`, which combines `results1` and `results2`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "results = results1.combine_first(results2)\n",
    "results.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can plot `theta` for both phases."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_theta(results):\n",
    "    plot(results.theta, label='theta')\n",
    "    decorate(xlabel='Time (s)',\n",
    "             ylabel='Angle (rad)')\n",
    "    \n",
    "plot_theta(results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And `omega`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_omega(results):\n",
    "    plot(results.omega, label='omega', color='C1')\n",
    "    decorate(xlabel='Time (s)',\n",
    "             ylabel='Angular velocity (rad/s)')\n",
    "    \n",
    "plot_omega(results)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "subplot(2, 1, 1)\n",
    "plot_theta(results)\n",
    "subplot(2, 1, 2)\n",
    "plot_omega(results)\n",
    "savefig('figs/chap25-fig01.pdf')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Estimating friction\n",
    "\n",
    "Let's take the code from the previous section and wrap it in a function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "def run_two_phases(force, torque_friction, params):\n",
    "    \"\"\"Run both phases.\n",
    "    \n",
    "    force: force applied to the turntable\n",
    "    torque_friction: friction due to torque\n",
    "    params: Params object\n",
    "    \n",
    "    returns: TimeFrame of simulation results\n",
    "    \"\"\"\n",
    "    # put the specified parameters into the Params object\n",
    "    params = Params(params, force=force, torque_friction=torque_friction)\n",
    "\n",
    "    # run phase 1\n",
    "    system1 = make_system(params)\n",
    "    results1, details1 = run_ode_solver(system1, slope_func, \n",
    "                                          events=event_func1)\n",
    "\n",
    "    # get the final state from phase 1\n",
    "    t_0 = results1.last_label() * s\n",
    "    init2 = results1.last_row()\n",
    "    \n",
    "    # run phase 2\n",
    "    system2 = System(system1, t_0=t_0, init=init2, force=0*N)\n",
    "    results2, details2 = run_ode_solver(system2, slope_func, \n",
    "                                        events=event_func2)\n",
    "    \n",
    "    # combine and return the results\n",
    "    results = results1.combine_first(results2)\n",
    "    return TimeFrame(results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's test it with the same parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "force = 1*N\n",
    "torque_friction = 0.2*N*m\n",
    "results = run_two_phases(force, torque_friction, params)\n",
    "results.tail()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And check the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "theta_final = results.last_row().theta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's the error function we'll use with `root_bisect`.\n",
    "\n",
    "It takes a hypothetical value for `torque_friction` and returns the difference between `theta_final` and the observed duration of the first push, 1.5 radian."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "def error_func1(torque_friction, params):\n",
    "    \"\"\"Error function for root_scalar.\n",
    "    \n",
    "    torque_friction: hypothetical value\n",
    "    params: Params object\n",
    "    \n",
    "    returns: offset from target value\n",
    "    \"\"\"\n",
    "    force = 1 * N\n",
    "    results = run_two_phases(force, torque_friction, params)\n",
    "    theta_final = results.last_row().theta\n",
    "    print(torque_friction, theta_final)\n",
    "    return theta_final - 1.5 * radian"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Testing the error function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "guess1 = 0.1*N*m\n",
    "error_func1(guess1, params)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "guess2 = 0.3*N*m\n",
    "error_func1(guess2, params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And running `root_scalar`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = root_bisect(error_func1, [guess1, guess2], params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The result is the coefficient of friction that yields a total rotation of 1.5 radian."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "torque_friction = res.root"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's a test run with the estimated value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "force = 1 * N\n",
    "results = run_two_phases(force, torque_friction, params)\n",
    "theta_final = get_last_value(results.theta)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks good."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Animation\n",
    "\n",
    "\n",
    "Here's a draw function we can use to animate the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib.patches import Circle\n",
    "from matplotlib.patches import Arrow\n",
    "\n",
    "def draw_func(state, t):\n",
    "    theta, omega = state\n",
    "    \n",
    "    # draw a circle for the table\n",
    "    radius_disk = magnitude(params.radius_disk)\n",
    "    circle1 = Circle([0, 0], radius_disk)\n",
    "    plt.gca().add_patch(circle1)\n",
    "    \n",
    "    # draw a circle for the teapot\n",
    "    radius_pot = magnitude(params.radius_pot)\n",
    "    center = pol2cart(theta, radius_pot)\n",
    "    circle2 = Circle(center, 0.05, color='C1')\n",
    "    plt.gca().add_patch(circle2)\n",
    "\n",
    "    # make the aspect ratio 1\n",
    "    plt.axis('equal')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [],
   "source": [
    "state = results.first_row()\n",
    "draw_func(state, 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "animate(results, draw_func)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### Exercises\n",
    "\n",
    "Now finish off the example by estimating the force that delivers the teapot to the desired position.\n",
    "\n",
    "Write an error function that takes `force` and `params` and returns the offset from the desired angle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Test the error function with `force=1`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And run `root_bisect` to find the desired force."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [],
   "source": [
    "force = res.root\n",
    "results = run_two_phases(force, torque_friction, params)\n",
    "theta_final = get_last_value(results.theta)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [],
   "source": [
    "remaining_angle = np.pi - 1.5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Exercise:** Now suppose my friend pours 0.1 kg of tea and puts the teapot back on the turntable at distance 0.3 meters from the center.  If I ask for the tea back, how much force should they apply, over an arc of 0.5 radians, to make the teapot glide to a stop back in front of me?  You can assume that torque due to friction is proportional to the total mass of the teapot and the turntable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Solution goes here"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
